/*******************************************************************************
 * AAEngine
 * Copyright (c) 2010 Mike Jarosch
 *
 * Original source PushButton Engine:
 * Copyright (C) 2009 PushButton Labs, LLC
 * For more information see http://www.pushbuttonengine.com
 *
 * This file is licensed under the terms of the MIT license, which is included
 * in the "MIT License.txt" file at the root directory of this SDK.
 ******************************************************************************/

using System;
using System.Collections.Generic;
using System.Linq;
using System.Xml.Linq;
using AAEngine.Engine.Core;
using AAEngine.Engine.Debug;
using AAEngine.Engine.Serialization;

namespace AAEngine.Engine.Entities
{
    /// <summary>
    /// Default implementation of IEntity.
    /// </summary>
    /// <remarks>
    /// Please use New() to get at instances of Entity; this allows     
    /// us to pool entities at a later date if needed and do other tricks. Please     
    /// program against IEntity, not Entity, to avoid dependencies.
    /// </remarks>
    public class Entity : AAObject, IEntity
    {
        public static readonly string ENTITY_DESTROYED_EVENT = "AAEngine.Engine.Entities.Entity.ENTITY_DESTROYED_EVENT";

        class PendingComponent
        {
            public IEntityComponent Item { get; set; }
            public string Name { get; set; }
        }

        private IDictionary<string, IEntityComponent> _components;
        private bool _deferring;
        private IList<PendingComponent> _deferredComponents;
        private EventDispatcher _eventDispatcher;
        private PropertyInfo _tempPropertyInfo = new PropertyInfo();

        Entity()
        {
            _components = new Dictionary<string, IEntityComponent>();
            _deferredComponents = new List<PendingComponent>();
            _eventDispatcher = new EventDispatcher();
        }

        public static IEntity New()
        {
            return new Entity();
        }

        public bool Deferring
        {
            get { return _deferring; }
            set
            {
                if (_deferring && !value)
                {
                    // Resolve everything, and everything that that resolution triggers
                    var needReset = _deferredComponents.Count > 0;
                    while (_deferredComponents.Count > 0)
                    {
                        var pedingComponent = _deferredComponents.Shift();
                        pedingComponent.Item.Register(this, pedingComponent.Name);
                    }

                    // Mark deferring as done.
                    _deferring = false;

                    // Fire off the reset.
                    if (needReset)
                    {
                        DoResetComponents();
                    }
                }
                _deferring = value;
            }
        }

        public IEventDispatcher EventDispatcher
        {
            get { return _eventDispatcher; }
        }

#if WINDOWS_PHONE || XBOX
        public override void Initialize()
        {
            Initialize(null, null);
        }

        public override void Initialize(string name)
        {
            Initialize(name, null);
        }

        public override void Initialize(string name, string alias)
#else
        public override void Initialize(string name = null, string alias = null)
#endif
        {
            // Pass control up.
            base.Initialize(name, alias);

            // Resolve any pending components.
            _deferring = false;
        }

        /// <summary>
        /// Destroys the Entity by removing all components and unregistering it from         
        /// the name manager.
        /// </summary>
        public override void Destroy()
        {
            if (_eventDispatcher.HasEventListeners(ENTITY_DESTROYED_EVENT))
            {
                _eventDispatcher.DispatchEvent(new Event(ENTITY_DESTROYED_EVENT));
            }

            // Unregister our components.
            foreach (IEntityComponent component in _components.Values)
            {
                if (component.IsRegistered)
                {
                    component.Unregister();
                }
            }

            // And remove their references from the dictionary.
            _components.Clear();

            // Get out of the NameManager and other general cleanup stuff.
            base.Destroy();
        }

        public void Serialize(XElement xml)
        {
            XElement entity = xml.AddElement("entity");
            entity.SetAttribute("name", Name);
            if (Alias != null)
            {
                entity.SetAttribute("alias", Alias);
            }

            foreach (IEntityComponent component in _components.Values)
            {
                XElement node = entity.AddElement("component");
                node.SetAttribute("type", component.GetType().FullName);
                node.SetAttribute("name", component.Name);
                Serializer.Serialize(component, node);
            }
        }

#if WINDOWS_PHONE || XBOX
        public void Deserialize(XElement xml)
        {
            Deserialize(xml, true);
        }

        public void Deserialize(XElement xml, bool registerComponents)
#else
        public void Deserialize(XElement xml, bool registerComponents = true)
#endif
        {
            Serializer.SetCurrentEntity(this);

            bool oldDeferring = _deferring;
            _deferring = true;

            foreach (XElement componentNode in xml.Elements())
            {
                if (componentNode.Name != "component")
                {
                    Logger.Error(this, "Deserialize", "Found unexpected tag '" + componentNode.Name + "', only <component /> is valid, ignoring tag. Error in entity '" + Name + "'.");
                    continue;
                }

                string componentName = componentNode.GetAttribute("name");
                string componentClassName = componentNode.GetAttribute("type");
                IEntityComponent component = null;

                if (!String.IsNullOrEmpty(componentClassName))
                {
                    component = TypeUtility.Instantiate(componentClassName) as IEntityComponent;
                    if (component == null)
                    {
                        Logger.Error(this, "Deserialize", "Unable to instantiate component '" + componentName + "' of type '" + componentClassName + "' on entity '" + Name + "'.");
                        continue;
                    }

                    if (!AddComponent(component, componentName))
                    {
                        continue;
                    }
                }
                else
                {
                    component = LookupComponentByName(componentName);
                    if (component == null)
                    {
                        Logger.Error(this, "Deserialize", "No type specified for the component '" + componentName + "' and the component doesn't exists on a parent template for entity '" + Name + "'.");
                        continue;
                    }
                }

                Serializer.Deserialize(component, componentNode);
            }

            // TODO: Sets

            _deferring = oldDeferring;
        }

        public bool AddComponent(IEntityComponent component, string name)
        {
            // Add it to the dictionary.
            if (!DoAddComponent(component, name))
            {
                return false;
            }

            // If we are deferring registration, put it on the list.
            if (Deferring)
            {
                var p = new PendingComponent();
                p.Item = component;
                p.Name = name;
                _deferredComponents.Add(p);
                return true;
            }

            // We have to be careful w.r.t. adding components from another component.
            component.Register(this, name);

            // Fire off the reset.
            DoResetComponents();

            return true;
        }

        public void RemoveComponent(IEntityComponent component)
        {
            // Update the dictionary.
            if (!DoRemoveComponent(component))
                return;

            // Deal with pending.
            if (!component.IsRegistered)
            {
                // Remove it from the deferred list.
                for (int i = 0; i < _deferredComponents.Count; i++)
                {
                    if (_deferredComponents[i].Item != component)
                        continue;

                    // TODO: Forcibly call register/unregister to ensure onAdd/onRemove semantics?

                    _deferredComponents.RemoveAt(i);
                    break;
                }

                return;
            }

            component.Unregister();

            DoResetComponents();
        }

        //public T FindComponent<T>() where T : IEntityComponent
        //{
        //    foreach (IEntityComponent component in _components.Values)
        //    {
        //        if (component is T)
        //        {
        //            return (T)component;
        //        }
        //    }
        //    return default(T);
        //}

        //public T FindComponent<T>(string name) where T : IEntityComponent
        //{
        //    if (_components.ContainsKey(name))
        //    {
        //        return (T)_components[name];
        //    }
        //    return default(T);
        //}

        //public IEnumerable<T> FindComponents<T>() where T : IEntityComponent
        //{
        //    List<T> list = new List<T>();
        //    foreach (IEntityComponent component in _components.Values)
        //    {
        //        if (component is T)
        //        {
        //            list.Add((T)component);
        //        }
        //    }
        //    return list;
        //}

        public IEntityComponent LookupComponentByType(Type type)
        {
            foreach (IEntityComponent component in _components.Values)
            {
                if (type.IsAssignableFrom(component.GetType()))
                {
                    return component;
                }
            }
            return null;
        }

        public IEnumerable<IEntityComponent> LookupComponentsByType(Type type)
        {
            List<IEntityComponent> list = new List<IEntityComponent>();
            foreach (IEntityComponent component in _components.Values)
            {
                if (type.IsAssignableFrom(component.GetType()))
                {
                    list.Add(component);
                }
            }
            return list;
        }

        public IEntityComponent LookupComponentByName(string componentName)
        {
            if (_components.ContainsKey(componentName))
            {
                return _components[componentName];
            }
            return null;
        }

        public bool DoesPropertyExist(PropertyReference property)
        {
            return FindProperty(property, false, _tempPropertyInfo, true) != null;
        }

#if WINDOWS_PHONE || XBOX
        public object GetProperty(PropertyReference property)
        {
            return GetProperty(property, null);
        }

        public object GetProperty(PropertyReference property, object defaultValue)
#else
        public object GetProperty(PropertyReference property, object defaultValue = null)
#endif
        {
            PropertyInfo info = FindProperty(property, false, _tempPropertyInfo);
            object result = null;

            if (info != null)
            {
                result = info.GetValue();
            }
            else
            {
                result = defaultValue;
            }

            _tempPropertyInfo.Clear();

            return result;
        }

        public void SetProperty(PropertyReference property, object value)
        {
            PropertyInfo info = FindProperty(property, true, _tempPropertyInfo);

            if (info != null)
            {
                info.SetValue(value);
            }

            _tempPropertyInfo.Clear();
        }

        private bool DoAddComponent(IEntityComponent component, string name)
        {
            if (String.IsNullOrEmpty(name))
            {
                Logger.Warn(this, "AddComponent", "A component name was not specified. This might cause problems later.");
            }

            if (component.Owner != null)
            {
                Logger.Warn(this, "AddComponent", "The component " + name + " already has an owner. (" + Name + ")");
                return false;
            }

            if (_components.Keys.Contains(name))
            {
                Logger.Warn(this, "AddComponent", "A component with name " + name + " already exists on this entity. (" + Name + ")");
                return false;
            }

            component.Owner = this;
            _components[name] = component;
            return true;
        }

        private bool DoRemoveComponent(IEntityComponent component)
        {
            if (component.Owner != this)
            {
                Logger.Warn(this, "AddComponent", "This component " + component.Name + " is not owned by this entity. (" + Name + ")");
                return false;
            }
            if (!_components.Keys.Contains(component.Name))
            {
                Logger.Warn(this, "AddComponent", "This component " + component.Name + " was not found on this entity. (" + Name + ")");
                return false;
            }
            _components.Remove(component.Name);
            return true;
        }

        /// <summary>
        /// Call reset on all the registered components in this entity.
        /// </summary>
        private void DoResetComponents()
        {
            Deferring = true;
            foreach (IEntityComponent component in _components.Values)
            {
                if (!component.IsRegistered)
                    continue;
                component.Reset();
            }
            Deferring = false;
        }

#if WINDOWS_PHONE || XBOX
        private PropertyInfo FindProperty(PropertyReference reference)
        {
            return FindProperty(reference, false, null, false);
        }

        private PropertyInfo FindProperty(PropertyReference reference, bool willSet)
        {
            return FindProperty(reference, willSet, null, false);
        }

        private PropertyInfo FindProperty(PropertyReference reference, bool willSet, PropertyInfo providedPi)
        {
            return FindProperty(reference, willSet, providedPi, false);
        }

        private PropertyInfo FindProperty(PropertyReference reference, bool willSet, PropertyInfo providedPi, bool suppressErrors)
#else
        private PropertyInfo FindProperty(PropertyReference reference, bool willSet = false, PropertyInfo providedPi = null, bool suppressErrors = false)
#endif
        {
            if (reference == null || String.IsNullOrEmpty(reference.Property))
            {
                return null;
            }

            Profiler.Enter("Entity.FindProperty");

            if (providedPi == null)
            {
                providedPi = new PropertyInfo();
            }

            if (reference.CachedLookup != null && reference.CachedLookup.Length > 0)
            {
                string[] cl = reference.CachedLookup;
                object cachedWalk = LookupComponentByName(cl[0]);
                if (cachedWalk == null)
                {
                    if (!suppressErrors)
                    {
                        Logger.Warn(this, "FindProperty", "[#" + this.Name + "] Could not resolve component named '" + cl[0] + "' for property '" + reference.Property + "' with cached reference.");
                    }
                    Profiler.Exit("Entity.FindProperty");
                    return null;
                }

                for (int i = 1; i < cl.Length - 1; i++)
                {
                    cachedWalk = TypeUtility.GetProperty(cachedWalk, cl[i]);
                    if (cachedWalk == null)
                    {
                        if (!suppressErrors)
                        {
                            Logger.Warn(this, "FindProperty", "[#" + this.Name + "] Could not resolve component named '" + cl[i] + "' for property '" + reference.Property + "' with cached reference.");
                        }
                        Profiler.Exit("Entity.FindProperty");
                        return null;
                    }
                }

                PropertyInfo cachedPi = providedPi;
                cachedPi.PropertyParent = cachedWalk;
                cachedPi.PropertyName = (cl.Length > 1) ? cl[cl.Length - 1] : null;
                Profiler.Exit("Entity.FindProperty");
                return cachedPi;
            }

            string propertyName = reference.Property;
            string[] path = propertyName.Split('.');

            // Distinguish if it is a component reference (@), named object ref (#), or
            // an XML reference (!), and look up the first element in the path.
            bool isTemplateXml = false;
            string itemName = path[0];
            int curIdx = 1;
            char startChar = itemName[0];
            string curLookup = itemName.Substring(1);
            object parentElem;
            if (startChar == '@')
            {
                parentElem = LookupComponentByName(curLookup);
                if (parentElem == null)
                {
                    if (!suppressErrors)
                    {
                        Logger.Warn(this, "FindProperty", "[#" + this.Name + "] Could not resolve component named '" + curLookup + "' for property '" + reference.Property + "'.");
                    }
                    Profiler.Exit("Entity.FindProperty");
                    return null;
                }

                path[0] = curLookup;
                reference.CachedLookup = path;
            }
            else if (startChar == '#')
            {
                parentElem = AAGame.NameManager.Lookup(curLookup);
                if (parentElem == null)
                {
                    if (!suppressErrors)
                    {
                        Logger.Warn(this, "FindProperty", "[#" + this.Name + "] Could not resolve named object named '" + curLookup + "' for property '" + reference.Property + "'.");
                    }
                    Profiler.Exit("Entity.FindProperty");
                    return null;
                }

                curIdx++;
                curLookup = path[1];
                IEntityComponent comLookup = (parentElem as IEntity).LookupComponentByName(curLookup);
                if (comLookup == null)
                {
                    if (!suppressErrors)
                    {
                        Logger.Warn(this, "FindProperty", "[#" + this.Name + "] Could not find component named '" + curLookup + "' on named entity '" + (parentElem as IEntity).Name + "' for property '" + reference.Property + "'.");
                    }
                    Profiler.Exit("Entity.FindProperty");
                    return null;
                }
                parentElem = comLookup;
            }
            else if (startChar == '!')
            {
                // XML reference. Look it up inside the TemplateManager. We only support
                // templates and entities - no groups.

                parentElem = AAGame.TemplateManager.GetXml(curLookup, "template", "entity");
                if (parentElem == null)
                {
                    if (!suppressErrors)
                    {
                        Logger.Warn(this, "FindProperty", "[#" + this.Name + "] Could not find XML named '" + curLookup + "' for property '" + reference.Property + "'.");
                    }
                    Profiler.Exit("Entity.FindProperty");
                    return null;
                }

                // Try to find the specified component.
                curIdx++;
                XElement nextElem = null;
                foreach (XElement tag in (parentElem as XElement).Elements())
                {
                    if (tag.GetAttribute("name") == path[1])
                    {
                        nextElem = tag;
                        break;
                    }
                }

                // Create it if appropriate.
                if (nextElem == null && willSet)
                {
                    // Create component tag.
                    (parentElem as XElement).AddElement("component").SetAttribute("name", path[1]);

                    // Look it up again
                    foreach (XElement tag in (parentElem as XElement).Elements())
                    {
                        if (tag.GetAttribute("name") == path[1])
                        {
                            nextElem = tag;
                            break;
                        }
                    }
                }

                // Error if we don't have it!
                if (nextElem == null)
                {
                    if (!suppressErrors)
                    {
                        Logger.Warn(this, "FindProperty", "[#" + this.Name + "] Could not find component named '" + path[1] + "' in XML template '" + path[0].Substring(1) + "' for property '" + reference.Property + "'.");
                    }
                    Profiler.Exit("Entity.FindProperty");
                    return null;
                }

                // Get ready to search the rest.
                parentElem = nextElem;

                // Indicate we are dealing with xml.
                isTemplateXml = true;
            }
            else
            {
                if (!suppressErrors)
                {
                    Logger.Warn(this, "FindProperty", "[#" + this.Name + "] Got a property path that doesn't start with !, #, or @. Started with '" + startChar + "' for property '" + reference.Property + "'");
                }
                Profiler.Exit("Entity.FindProperty");
                return null;
            }

            // Make sure we have a field to look up.
            if (curIdx < path.Length)
            {
                curLookup = path[curIdx++];
            }
            else
            {
                curLookup = null;
            }

            // Do the remainder of the look up.
            while (curIdx < path.Length && parentElem != null)
            {
                // Try the next element in the path.
                object oldParentElem = parentElem;
                try
                {
                    if (parentElem is XElement)
                    {
                        parentElem = (parentElem as XElement).Elements(curLookup);
                    }
                    else if (parentElem is IEnumerable<XElement>)
                    {
                        parentElem = (parentElem as IEnumerable<XElement>).ElementAt(Convert.ToInt32(curLookup));
                    }
                    else
                    {
                        parentElem = TypeUtility.GetProperty(parentElem, curLookup);
                    }
                }
                catch(Exception)
                {
                    parentElem = null;
                }

                // Several different possibilities that indicate we failed to advance.
                bool gotEmpty = false;
                if (parentElem == null)
                {
                    gotEmpty = true;
                }
                if (parentElem is IEnumerable<XElement> && (parentElem as IEnumerable<XElement>).Count() == 0)
                {
                    gotEmpty = true;
                }

                // If we're going to set and it's XML, create the field.
                if (willSet && isTemplateXml && gotEmpty && oldParentElem != null)
                {
                    parentElem = (oldParentElem as XElement).AddElement(curLookup);
                    gotEmpty = false;
                }

                if (gotEmpty)
                {
                    if (!suppressErrors)
                    {
                        Logger.Warn(this, "FindProperty", "[#" + this.Name + "] Could not resolve property '" + curLookup + "' for property reference '" + reference.Property + "'");
                    }
                    Profiler.Exit("Entity.FindProperty");
                    return null;
                }

                // Advance to next element in the path.
                curLookup = path[curIdx++];
            }

            // Did we end up with a match?
            if(parentElem != null)
            {
                PropertyInfo pi = providedPi;
                pi.PropertyParent = parentElem;
                pi.PropertyName = curLookup;
                Profiler.Exit("Entity.FindProperty");
                return pi;
            }

            Profiler.Exit("Entity.FindProperty");
            return null;
        }
    }
}
